<template>
    <div class="container">
        <h1>在线Base64加密解密</h1>
        <div class="note">提示：编码会将左侧明文转换为 Base64 并填入右侧；解码会将右侧 Base64 解码为明文并填入左侧输入框。</div>
        <div v-if="error" class="error">{{ error }}</div>
        <div class="panel">
            <div class="box">
                <label for="plain">明文</label>
                <textarea id="plain" v-model="plain" placeholder="在此输入明文（可包含中文/Unicode）"></textarea>
            </div>

            <div class="controls" aria-hidden="false">
                <button @click="encode" title="Base64 编码" aria-label="编码 (左到右)">
                    编码 →
                </button>
                <button @click="decode" title="Base64 解码" aria-label="解码 (右到左)">
                    ← 解码
                </button>
                <button class="swap" @click="swap" title="交换" aria-label="交换">
                    ⇄ 交换
                </button>
            </div>

            <div class="box">
                <label for="base64">Base64</label>
                <textarea id="base64" v-model="base64" placeholder="在此输入 Base64 密文"></textarea>
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const plain = ref<string>('')
const base64 = ref<string>('')
const error = ref<string>('')

function toBase64(str: string): string {
    // Properly handle Unicode by encoding to UTF-8 bytes first
    const encoder = new TextEncoder()
    const bytes = encoder.encode(str)
    let binary = ''
    for (let i = 0; i < bytes.length; i++) {
        binary += String.fromCharCode(bytes[i]!)
    }
    return btoa(binary)
}

function fromBase64(b64: string): string {
    // atob -> bytes -> decode UTF-8
    const binary = atob(b64)
    const len = binary.length
    const bytes = new Uint8Array(len)
    for (let i = 0; i < len; i++) {
        bytes[i] = binary.charCodeAt(i)
    }
    const decoder = new TextDecoder()
    return decoder.decode(bytes)
}

function encode() {
    error.value = ''
    try {
        base64.value = toBase64(plain.value)
    } catch (e: unknown) {
        error.value = '编码失败：' + (e && e instanceof Error ? e.message : String(e))
    }
}

function decode() {
    error.value = ''
    try {
        // trim to avoid accidental whitespace causing atob errors
        plain.value = fromBase64(base64.value.trim())
    } catch (e: unknown) {
        console.error(e)
        error.value = '解码失败：输入可能不是有效的 Base64 文本。'
    }
}

function swap() {
    const p = plain.value
    plain.value = fromMaybeBase64(base64.value)
    base64.value = toMaybeBase64(p)
}

function fromMaybeBase64(b: string): string {
    // try decode, otherwise return original as plain text
    try {
        return fromBase64(b.trim())
    } catch {
        return b
    }
}

function toMaybeBase64(s: string): string {
    try {
        return toBase64(s)
    } catch {
        return s
    }
}
</script>

<style scoped>
:root {
    --gap: 12px;
    --controls-width: 110px;
}

html,
body,
#app {
    height: 100%;
    margin: 0;
}

.container {
    display: flex;
    flex-direction: column;
    height: 100vh;
    /* make container fill viewport for full-height responsiveness */
    max-width: 90%;
    margin: 0 auto;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
    color: #222;
    padding: 16px;
    box-sizing: border-box;
    gap: 8px;
}

h1 {
    margin: 0;
    font-size: 20px;
    text-align: center;
    padding-bottom: 8px;
}

.panel {
    display: flex;
    gap: var(--gap);
    align-items: stretch;
    flex: 1;
    /* let panel expand to take available space */
    min-height: 0;
    /* allow children to shrink properly */
    margin-bottom: 200px;
}

.box {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-height: 0;
    /* important so textarea can scroll within flex children */
}

label {
    font-size: 13px;
    margin-bottom: 6px;
}

textarea {
    width: 100%;
    /* make textarea fill available vertical space inside its box */
    flex: 1;
    min-height: 0;
    resize: none;
    /* use scrollbar instead of manual resize for consistent layout */
    padding: 8px;
    font-size: 14px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace;
    border: 1px solid #d0d7de;
    border-radius: 6px;
    box-sizing: border-box;
    overflow: auto;
}

.controls {
    width: var(--controls-width);
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    margin: 0px 30px 0px 30px;
}

.controls button {
    width: 100%;
    padding: 8px 12px;
    border-radius: 6px;
    border: 1px solid #c9d1d9;
    background: #f6f8fa;
    cursor: pointer;
    font-weight: 600;
    white-space: nowrap;
}

.controls button:hover {
    background: #eef3f7;
}

.controls .swap {
    background: #fffbe6;
}

.error {
    margin-top: 6px;
    color: #c53030;
    font-weight: 600;
}

.note {
    margin-top: 6px;
    color: #4b5563;
    font-size: 18px;
}

/* Responsive adjustments for narrow screens */
@media (max-width: 720px) {
    .panel {
        flex-direction: column;
        gap: 8px;
        padding-bottom: 8px;
    }

    .controls {
        flex-direction: row;
        width: 100%;
        justify-content: center;
    }

    .controls button {
        width: auto;
        min-width: 80px;
    }
}
</style>